
LLVM具有几个消毒器，这些通道使用中间表示(IR)来检查应用程序的某些错误行为。通常，它们需要库支持，这是compiler-rt项目的一部分。消毒器可以在clang中启用，要构建编译器-rt项目，可以简单地在构建LLVM时将CMake变量-DLLVM\_ENABLE\_RUNTIMES=compiler-rt添加到CMake配置步骤中。

下面几节中，将研究地址、内存和线程消毒器。首先，来看看地址消毒器。

\mySubsubsection{10.2.1.}{使用地址消毒器检测内存访问问题}

可以使用地址杀毒器，来检测应用程序中不同类型的内存访问错误。这包括常见的错误，例如在释放动态分配的内存后使用，或者在已分配内存的边界外写入动态分配的内存。

启用后，地址消毒器会替换对malloc()和free()函数的调用，并使用检查保护来检测所有内存访问。这会给应用程序增加很多开销，而且只会在应用程序的测试阶段使用地址消毒器。若对实现细节感兴趣，可以在llvm/lib/Transforms/Instrumentation/AddressSanitzer.cpp中为通道的实现源码，实现算法的描述见\url{https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm}。

让我们运行一个简短的示例来展示地址消毒器的功能!

下面的示例应用程序outfbounds.c分配了12个字节的内存，但初始化了14个字节:

\begin{cpp}
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char *p = malloc(12);
    memset(p, 0, 14);
    return (int)*p;
}
\end{cpp}

可以编译并运行此应用程序而不会发现任何问题，因为这种行为是此类错误的典型表现。即使在较大的应用程序中，这类错误也可能在很长一段时间内被忽视。但若使用-fsanitize=address选项启用地址消毒器，则应用程序在检测到错误后停止。

使用-g选项启用调试符号也很有用，它有助于识别源代码中错误的位置。以下代码是如何在启用地址消毒器和调试符号的情况下编译源文件的示例:

\begin{shell}
$ clang -fsanitize=address -g outofbounds.c -o outofbounds
\end{shell}

现在，当运行应用程序时，将会得到一个冗长的错误报告:

\begin{shell}
$ ./outofbounds
==============================================================
==1067==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x60200000001c at pc 0x00000023a6ef bp 0x7fffffffeb10 sp
0x7fffffffe2d8
WRITE of size 14 at 0x60200000001c thread T0
    #0 0x23a6ee in __asan_memset /usr/src/contrib/llvm-project/
compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp:26:3
    #1 0x2b2a03 in main /home/kai/sanitizers/outofbounds.c:6:3
    #2 0x23331f in _start /usr/src/lib/csu/amd64/crt1.c:76:7
\end{shell}

该报告还包含有关内存内容的详细信息。重要的信息是错误的类型(在本例中是堆缓冲区溢出)和出错的源代码行。要找到源代码行，必须查看位置\#1处的堆栈跟踪，这是地址消毒器拦截应用程序执行之前的最后一个位置——outfbounds.c文件中的第6行，这是包含对memset()调用的行，这就是缓冲区溢出发生的确切位置。

若替换包含memset(p, 0, 14);在带有以下代码的outfbounds.c文件中，当释放了内存，就可以引入对内存的访问，需要将源代码存储在useafterfree.c文件中:

\begin{cpp}
memset(p, 0, 12);
free(p);
\end{cpp}

同样，若编译并运行它，消毒器会在释放内存后检测指针的使用:

\begin{shell}
$ clang -fsanitize=address -g useafterfree.c -o useafterfree
$ ./useafterfree
==============================================================
==1118==ERROR: AddressSanitizer: heap-use-after-free on address
0x602000000010 at pc 0x0000002b2a5c bp 0x7fffffffeb00 sp
0x7fffffffeaf8
READ of size 1 at 0x602000000010 thread T0
    #0 0x2b2a5b in main /home/kai/sanitizers/useafterfree.c:8:15
    #1 0x23331f in _start /usr/src/lib/csu/amd64/crt1.c:76:7
\end{shell}

这次，报告指向第8行，其中包含对p指针的解引用。

x86\_64 Linux和macOS上，还可以启用泄漏检测，若将ASAN\_OPTIONS环境变量设置为在运行应用程序之前detect\_leaks=1，还将获得关于内存泄漏的报告。

命令行中，可以这样做:

\begin{shell}
$ ASAN_OPTIONS=detect_leaks=1 ./useafterfree
\end{shell}

地址消毒器非常有用，它捕获了其他方法很难检测到的一类错误。内存消毒器执行类似的任务，我们将在下一节中研究它的用例。

\mySubsubsection{10.2.2.}{使用内存消毒器查找未初始化的内存访问}

使用未初始化的内存是另一类很难发现的bug。在C和C++中，一般的内存分配例程不会用默认值初始化内存缓冲区，对于堆栈上的自动变量也是如此。

有很多出错的机会，内存消毒器可以帮助找到这些错误。若对实现细节感兴趣，可以在llvm/lib/Transforms/Instrumentation/MemorySanitizer.cpp文件中找到内存消毒通道的源码。文件顶部的注释解释了实现背后的思想。

让我们运行一个小示例，并将以下源代码保存为memory.c文件。注意，变量x没有初始化，而是用作返回值:

\begin{cpp}
int main(int argc, char *argv[]) {
    int x;
    return x;
}
\end{cpp}

没启用杀菌器时，应用程序将运行良好。若使用-fsanitize=memory选项，将会得到一个错误报告:

\begin{shell}
$ clang -fsanitize=memory -g memory.c -o memory
$ ./memory
==1206==WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 0x10a8f49 in main /home/kai/sanitizers/memory.c:3:3
    #1 0x1053481 in _start /usr/src/lib/csu/amd64/crt1.c:76:7
SUMMARY: MemorySanitizer: use-of-uninitialized-value /home/kai/
sanitizers/memory.c:3:3 in main
Exiting
\end{shell}

与地址消毒器一样，内存消毒器在发现第一个错误时停止应用程序。如图所示，内存杀毒器提供了一个使用初始化值的警告。

下一节中，我们将研究如何使用线程消毒器来检测多线程应用程序中的数据争用。

\mySubsubsection{10.2.3.}{使用线程消毒器发现数据竞争}

为了利用现代CPU的强大功能，应用程序现在使用多线程。这是一种强大的技术，但也引入了新的错误。多线程应用程序中一个非常常见的问题是，对全局数据的访问没有受到保护，例如：使用互斥锁或信号量。

这就是数据竞争。线程消毒器可以检测基于pthread的应用程序和使用LLVM lib++实现的应用程序中的数据竞争，可以在llvm/lib/Transforms/Instrumentation/ThreadSanitizer.cpp文件中找到实现。为了演示线程消毒器的功能，将创建一个非常简单的生产者-消费者风格的应用程序。生产者线程增加一个全局变量，而消费者线程减少同一个变量。对全局变量的访问不受保护，因此这是一种数据竞争。

需要在thread.c文件中书写以下源代码:

\begin{cpp}
#include <pthread.h>

int data = 0;

void *producer(void *x) {
    for (int i = 0; i < 10000; ++i) ++data;
    return x;
}

void *consumer(void *x) {
    for (int i = 0; i < 10000; ++i) --data;
    return x;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, producer, NULL);
    pthread_create(&t2, NULL, consumer, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    return data;
}
\end{cpp}

前面的代码中，data变量在两个线程之间共享。为了使示例简单，它是int类型，通常会使用std::vector类或类似的数据结构，这两个线程运行producer()和consumer()函数。

producer()函数只增加数据变量，而consumer()函数则减少数据变量。没有实现访问保护，因此这构成了数据竞争。main()函数使用pthread\_create()函数启动两个线程，使用pthread\_join()函数等待线程结束，并返回data变量的当前值。

若编译并运行此应用程序，则不会注意到错误——返回值始终为零。若执行的循环次数增加100倍，将出现一个错误——在本例中，返回值不等于0。此时，需要注意出现的其他值。

可以使用线程消毒器来识别程序中的数据争用。要在启用线程消毒器的情况下进行编译，需要将-fsanitize=thread选项传递给clang。使用-g选项添加调试符号可以在报告中显示行号，还需要链接pthread库:

\begin{shell}
$ clang -fsanitize=thread -g thread.c -o thread -lpthread
$ ./thread
==================
WARNING: ThreadSanitizer: data race (pid=1474)
    Write of size 4 at 0x000000cdf8f8 by thread T2:
        #0 consumer /home/kai/sanitizers/thread.c:11:35 (thread+0x2b0fb2)

    Previous write of size 4 at 0x000000cdf8f8 by thread T1:
        #0 producer /home/kai/sanitizers/thread.c:6:35 (thread+0x2b0f22)

    Location is global 'data' of size 4 at 0x000000cdf8f8
(thread+0x000000cdf8f8)

    Thread T2 (tid=100437, running) created by main thread at:
        #0 pthread_create /usr/src/contrib/llvm-project/compiler-rt/lib/
tsan/rtl/tsan_interceptors_posix.cpp:962:3 (thread+0x271703)
        #1 main /home/kai/sanitizers/thread.c:18:3 (thread+0x2b1040)

Thread T1 (tid=100436, finished) created by main thread at:
    #0 pthread_create /usr/src/contrib/llvm-project/compiler-rt/lib/
tsan/rtl/tsan_interceptors_posix.cpp:962:3 (thread+0x271703)
    #1 main /home/kai/sanitizers/thread.c:17:3 (thread+0x2b1021)

SUMMARY: ThreadSanitizer: data race /home/kai/sanitizers/
thread.c:11:35 in consumer
==================
ThreadSanitizer: reported 1 warnings
\end{shell}

报告指向源文件的第6行和第11行，在这里访问全局变量。它还显示了名为T1和T2的两个线程，访问了变量以及分别调用pthread\_create()函数的文件和行号。

在此基础上，我们了解了如何使用三种不同类型的消毒器来识别应用程序中的常见问题。地址消毒器可识别常见的内存访问错误，例如：越界访问或在内存被释放后使用内存。使用内存消毒器，可以找到对未初始化内存的访问，线程消毒器可检查数据竞争。

下一节中，我们将尝试通过在随机数据上运行应用程序来触发消毒器，这个过程称为模糊测试。



